<!-- directive:breadcrumb Object.is() -->
<!-- directive:title 对象的新增方法 -->
<div class='panel-body'>
    <p>ES5 比较两个值是否相等，只有两个运算符：相等运算符（==）和严格相等运算符（===）。它们都有缺点，前者会自动转换数据类型，后者的NaN不等于自身，以及+0等于-0。JavaScript 缺乏一种运算，在所有环境中，只要两个值是一样的，它们就应该相等。</p>
    <p>ES6 提出“Same-value equality”（同值相等）算法，用来解决这个问题。Object.is就是部署这个算法的新方法。它用来比较两个值是否严格相等，与严格比较运算符（===）的行为基本一致。</p>
    <p>Object.is('foo', 'foo')</p>
    <p>// true</p>
    <p>Object.is({}, {})</p>
    <p>// false</p>
    <p>不同之处只有两个：一是+0不等于-0，二是NaN等于自身。</p>
    <p>+0 === -0 //true</p>
    <p>NaN === NaN // false</p>
    <p>Object.is(+0, -0) // false</p>
    <p>Object.is(NaN, NaN) // true</p>
    <p>ES5 可以通过下面的代码，部署Object.is。</p>
    <p>Object.defineProperty(Object, 'is', {</p>
    <p>    value: function(x, y) {</p>
    <p>        if (x === y) {</p>
    <p>            // 针对+0 不等于 -0的情况</p>
    <p>            return x !== 0 || 1 / x === 1 / y;</p>
    <p>        }</p>
    <p>        // 针对NaN的情况</p>
    <p>        return x !== x && y !== y;</p>
    <p>    },</p>
    <p>    configurable: true,</p>
    <p>    enumerable: false,</p>
    <p>    writable: true</p>
    <p>});</p>
    <p>Object.assign()</p>
    <p>基本用法</p>
    <p>Object.assign方法用于对象的合并，将源对象（source）的所有可枚举属性，复制到目标对象（target）。</p>
    <p>const target = { a: 1 };</p>
    <p>const source1 = { b: 2 };</p>
    <p>const source2 = { c: 3 };</p>
    <p>Object.assign(target, source1, source2);</p>
    <p>target // {a:1, b:2, c:3}</p>
    <p>Object.assign方法的第一个参数是目标对象，后面的参数都是源对象。</p>
    <p>注意，如果目标对象与源对象有同名属性，或多个源对象有同名属性，则后面的属性会覆盖前面的属性。</p>
    <p>const target = { a: 1, b: 1 };</p>
    <p>const source1 = { b: 2, c: 2 };</p>
    <p>const source2 = { c: 3 };</p>
    <p>Object.assign(target, source1, source2);</p>
    <p>target // {a:1, b:2, c:3}</p>
    <p>如果只有一个参数，Object.assign会直接返回该参数。</p>
    <p>const obj = {a: 1};</p>
    <p>Object.assign(obj) === obj // true</p>
    <p>如果该参数不是对象，则会先转成对象，然后返回。</p>
    <p>typeof Object.assign(2) // "object"</p>
    <p>由于undefined和null无法转成对象，所以如果它们作为参数，就会报错。</p>
    <p>Object.assign(undefined) // 报错</p>
    <p>Object.assign(null) // 报错</p>
    <p>如果非对象参数出现在源对象的位置（即非首参数），那么处理规则有所不同。首先，这些参数都会转成对象，如果无法转成对象，就会跳过。这意味着，如果undefined和null不在首参数，就不会报错。</p>
    <p>let obj = {a: 1};</p>
    <p>Object.assign(obj, undefined) === obj // true</p>
    <p>Object.assign(obj, null) === obj // true</p>
    <p>其他类型的值（即数值、字符串和布尔值）不在首参数，也不会报错。但是，除了字符串会以数组形式，拷贝入目标对象，其他值都不会产生效果。</p>
    <p>const v1 = 'abc';</p>
    <p>const v2 = true;</p>
    <p>const v3 = 10;</p>
    <p>const obj = Object.assign({}, v1, v2, v3);</p>
    <p>console.log(obj); // { "0": "a", "1": "b", "2": "c" }</p>
    <p>上面代码中，v1、v2、v3分别是字符串、布尔值和数值，结果只有字符串合入目标对象（以字符数组的形式），数值和布尔值都会被忽略。这是因为只有字符串的包装对象，会产生可枚举属性。</p>
    <p>Object(true) // {[[PrimitiveValue]]: true}</p>
    <p>Object(10)    //    {[[PrimitiveValue]]: 10}</p>
    <p>Object('abc') // {0: "a", 1: "b", 2: "c", length: 3, [[PrimitiveValue]]: "abc"}</p>
    <p>上面代码中，布尔值、数值、字符串分别转成对应的包装对象，可以看到它们的原始值都在包装对象的内部属性[[PrimitiveValue]]上面，这个属性是不会被Object.assign拷贝的。只有字符串的包装对象，会产生可枚举的实义属性，那些属性则会被拷贝。</p>
    <p>Object.assign拷贝的属性是有限制的，只拷贝源对象的自身属性（不拷贝继承属性），也不拷贝不可枚举的属性（enumerable: false）。</p>
    <p>Object.assign({b: 'c'},</p>
    <p>    Object.defineProperty({}, 'invisible', {</p>
    <p>        enumerable: false,</p>
    <p>        value: 'hello'</p>
    <p>    })</p>
    <p>)</p>
    <p>// { b: 'c' }</p>
    <p>上面代码中，Object.assign要拷贝的对象只有一个不可枚举属性invisible，这个属性并没有被拷贝进去。</p>
    <p>属性名为 Symbol 值的属性，也会被Object.assign拷贝。</p>
    <p>Object.assign({ a: 'b' }, { [Symbol('c')]: 'd' })</p>
    <p>// { a: 'b', Symbol(c): 'd' }</p>
    <p>注意点</p>
    <p>（1）浅拷贝</p>
    <p>Object.assign方法实行的是浅拷贝，而不是深拷贝。也就是说，如果源对象某个属性的值是对象，那么目标对象拷贝得到的是这个对象的引用。</p>
    <p>const obj1 = {a: {b: 1}};</p>
    <p>const obj2 = Object.assign({}, obj1);</p>
    <p>obj1.a.b = 2;</p>
    <p>obj2.a.b // 2</p>
    <p>上面代码中，源对象obj1的a属性的值是一个对象，Object.assign拷贝得到的是这个对象的引用。这个对象的任何变化，都会反映到目标对象上面。</p>
    <p>（2）同名属性的替换</p>
    <p>对于这种嵌套的对象，一旦遇到同名属性，Object.assign的处理方法是替换，而不是添加。</p>
    <p>const target = { a: { b: 'c', d: 'e' } }</p>
    <p>const source = { a: { b: 'hello' } }</p>
    <p>Object.assign(target, source)</p>
    <p>// { a: { b: 'hello' } }</p>
    <p>上面代码中，target对象的a属性被source对象的a属性整个替换掉了，而不会得到{ a: { b: 'hello', d: 'e' } }的结果。这通常不是开发者想要的，需要特别小心。</p>
    <p>一些函数库提供Object.assign的定制版本（比如 Lodash 的_.defaultsDeep方法），可以得到深拷贝的合并。</p>
    <p>（3）数组的处理</p>
    <p>Object.assign可以用来处理数组，但是会把数组视为对象。</p>
    <p>Object.assign([1, 2, 3], [4, 5])</p>
    <p>// [4, 5, 3]</p>
    <p>上面代码中，Object.assign把数组视为属性名为 0、1、2 的对象，因此源数组的 0 号属性4覆盖了目标数组的 0 号属性1。</p>
    <p>（4）取值函数的处理</p>
    <p>Object.assign只能进行值的复制，如果要复制的值是一个取值函数，那么将求值后再复制。</p>
    <p>const source = {</p>
    <p>    get foo() { return 1 }</p>
    <p>};</p>
    <p>const target = {};</p>
    <p>Object.assign(target, source)</p>
    <p>// { foo: 1 }</p>
    <p>上面代码中，source对象的foo属性是一个取值函数，Object.assign不会复制这个取值函数，只会拿到值以后，将这个值复制过去。</p>
    <p>常见用途</p>
    <p>Object.assign方法有很多用处。</p>
    <p>（1）为对象添加属性</p>
    <p>class Point {</p>
    <p>    constructor(x, y) {</p>
    <p>        Object.assign(this, {x, y});</p>
    <p>    }</p>
    <p>}</p>
    <p>上面方法通过Object.assign方法，将x属性和y属性添加到Point类的对象实例。</p>
    <p>（2）为对象添加方法</p>
    <p>Object.assign(SomeClass.prototype, {</p>
    <p>    someMethod(arg1, arg2) {</p>
    <p>        ···</p>
    <p>    },</p>
    <p>    anotherMethod() {</p>
    <p>        ···</p>
    <p>    }</p>
    <p>});</p>
    <p>// 等同于下面的写法</p>
    <p>SomeClass.prototype.someMethod = function (arg1, arg2) {</p>
    <p>    ···</p>
    <p>};</p>
    <p>SomeClass.prototype.anotherMethod = function () {</p>
    <p>    ···</p>
    <p>};</p>
    <p>上面代码使用了对象属性的简洁表示法，直接将两个函数放在大括号中，再使用assign方法添加到SomeClass.prototype之中。</p>
    <p>（3）克隆对象</p>
    <p>function clone(origin) {</p>
    <p>    return Object.assign({}, origin);</p>
    <p>}</p>
    <p>上面代码将原始对象拷贝到一个空对象，就得到了原始对象的克隆。</p>
    <p>不过，采用这种方法克隆，只能克隆原始对象自身的值，不能克隆它继承的值。如果想要保持继承链，可以采用下面的代码。</p>
    <p>function clone(origin) {</p>
    <p>    let originProto = Object.getPrototypeOf(origin);</p>
    <p>    return Object.assign(Object.create(originProto), origin);</p>
    <p>}</p>
    <p>（4）合并多个对象</p>
    <p>将多个对象合并到某个对象。</p>
    <p>const merge =</p>
    <p>    (target, ...sources) => Object.assign(target, ...sources);</p>
    <p>如果希望合并后返回一个新对象，可以改写上面函数，对一个空对象合并。</p>
    <p>const merge =</p>
    <p>    (...sources) => Object.assign({}, ...sources);</p>
    <p>（5）为属性指定默认值</p>
    <p>const DEFAULTS = {</p>
    <p>    logLevel: 0,</p>
    <p>    outputFormat: 'html'</p>
    <p>};</p>
    <p>function processContent(options) {</p>
    <p>    options = Object.assign({}, DEFAULTS, options);</p>
    <p>    console.log(options);</p>
    <p>    // ...</p>
    <p>}</p>
    <p>上面代码中，DEFAULTS对象是默认值，options对象是用户提供的参数。Object.assign方法将DEFAULTS和options合并成一个新对象，如果两者有同名属性，则option的属性值会覆盖DEFAULTS的属性值。</p>
    <p>注意，由于存在浅拷贝的问题，DEFAULTS对象和options对象的所有属性的值，最好都是简单类型，不要指向另一个对象。否则，DEFAULTS对象的该属性很可能不起作用。</p>
    <p>const DEFAULTS = {</p>
    <p>    url: {</p>
    <p>        host: 'example.com',</p>
    <p>        port: 7070</p>
    <p>    },</p>
    <p>};</p>
    <p>processContent({ url: {port: 8000} })</p>
    <p>// {</p>
    <p>//     url: {port: 8000}</p>
    <p>// }</p>
    <p>上面代码的原意是将url.port改成 8000，url.host不变。实际结果却是options.url覆盖掉DEFAULTS.url，所以url.host就不存在了。</p>
    <p>Object.getOwnPropertyDescriptors()</p>
    <p>ES5 的Object.getOwnPropertyDescriptor()方法会返回某个对象属性的描述对象（descriptor）。ES2017 引入了Object.getOwnPropertyDescriptors()方法，返回指定对象所有自身属性（非继承属性）的描述对象。</p>
    <p>const obj = {</p>
    <p>    foo: 123,</p>
    <p>    get bar() { return 'abc' }</p>
    <p>};</p>
    <p>Object.getOwnPropertyDescriptors(obj)</p>
    <p>// { foo:</p>
    <p>//        { value: 123,</p>
    <p>//            writable: true,</p>
    <p>//            enumerable: true,</p>
    <p>//            configurable: true },</p>
    <p>//     bar:</p>
    <p>//        { get: [Function: get bar],</p>
    <p>//            set: undefined,</p>
    <p>//            enumerable: true,</p>
    <p>//            configurable: true } }</p>
    <p>上面代码中，Object.getOwnPropertyDescriptors()方法返回一个对象，所有原对象的属性名都是该对象的属性名，对应的属性值就是该属性的描述对象。</p>
    <p>该方法的实现非常容易。</p>
    <p>function getOwnPropertyDescriptors(obj) {</p>
    <p>    const result = {};</p>
    <p>    for (let key of Reflect.ownKeys(obj)) {</p>
    <p>        result[key] = Object.getOwnPropertyDescriptor(obj, key);</p>
    <p>    }</p>
    <p>    return result;</p>
    <p>}</p>
    <p>该方法的引入目的，主要是为了解决Object.assign()无法正确拷贝get属性和set属性的问题。</p>
    <p>const source = {</p>
    <p>    set foo(value) {</p>
    <p>        console.log(value);</p>
    <p>    }</p>
    <p>};</p>
    <p>const target1 = {};</p>
    <p>Object.assign(target1, source);</p>
    <p>Object.getOwnPropertyDescriptor(target1, 'foo')</p>
    <p>// { value: undefined,</p>
    <p>//     writable: true,</p>
    <p>//     enumerable: true,</p>
    <p>//     configurable: true }</p>
    <p>上面代码中，source对象的foo属性的值是一个赋值函数，Object.assign方法将这个属性拷贝给target1对象，结果该属性的值变成了undefined。这是因为Object.assign方法总是拷贝一个属性的值，而不会拷贝它背后的赋值方法或取值方法。</p>
    <p>这时，Object.getOwnPropertyDescriptors()方法配合Object.defineProperties()方法，就可以实现正确拷贝。</p>
    <p>const source = {</p>
    <p>    set foo(value) {</p>
    <p>        console.log(value);</p>
    <p>    }</p>
    <p>};</p>
    <p>const target2 = {};</p>
    <p>Object.defineProperties(target2, Object.getOwnPropertyDescriptors(source));</p>
    <p>Object.getOwnPropertyDescriptor(target2, 'foo')</p>
    <p>// { get: undefined,</p>
    <p>//     set: [Function: set foo],</p>
    <p>//     enumerable: true,</p>
    <p>//     configurable: true }</p>
    <p>上面代码中，两个对象合并的逻辑可以写成一个函数。</p>
    <p>const shallowMerge = (target, source) => Object.defineProperties(</p>
    <p>    target,</p>
    <p>    Object.getOwnPropertyDescriptors(source)</p>
    <p>);</p>
    <p>Object.getOwnPropertyDescriptors()方法的另一个用处，是配合Object.create()方法，将对象属性克隆到一个新对象。这属于浅拷贝。</p>
    <p>const clone = Object.create(Object.getPrototypeOf(obj),</p>
    <p>    Object.getOwnPropertyDescriptors(obj));</p>
    <p>// 或者</p>
    <p>const shallowClone = (obj) => Object.create(</p>
    <p>    Object.getPrototypeOf(obj),</p>
    <p>    Object.getOwnPropertyDescriptors(obj)</p>
    <p>);</p>
    <p>上面代码会克隆对象obj。</p>
    <p>另外，Object.getOwnPropertyDescriptors()方法可以实现一个对象继承另一个对象。以前，继承另一个对象，常常写成下面这样。</p>
    <p>const obj = {</p>
    <p>    __proto__: prot,</p>
    <p>    foo: 123,</p>
    <p>};</p>
    <p>ES6 规定__proto__只有浏览器要部署，其他环境不用部署。如果去除__proto__，上面代码就要改成下面这样。</p>
    <p>const obj = Object.create(prot);</p>
    <p>obj.foo = 123;</p>
    <p>// 或者</p>
    <p>const obj = Object.assign(</p>
    <p>    Object.create(prot),</p>
    <p>    {</p>
    <p>        foo: 123,</p>
    <p>    }</p>
    <p>);</p>
    <p>有了Object.getOwnPropertyDescriptors()，我们就有了另一种写法。</p>
    <p>const obj = Object.create(</p>
    <p>    prot,</p>
    <p>    Object.getOwnPropertyDescriptors({</p>
    <p>        foo: 123,</p>
    <p>    })</p>
    <p>);</p>
    <p>Object.getOwnPropertyDescriptors()也可以用来实现 Mixin（混入）模式。</p>
    <p>let mix = (object) => ({</p>
    <p>    with: (...mixins) => mixins.reduce(</p>
    <p>        (c, mixin) => Object.create(</p>
    <p>            c, Object.getOwnPropertyDescriptors(mixin)</p>
    <p>        ), object)</p>
    <p>});</p>
    <p>// multiple mixins example</p>
    <p>let a = {a: 'a'};</p>
    <p>let b = {b: 'b'};</p>
    <p>let c = {c: 'c'};</p>
    <p>let d = mix(c).with(a, b);</p>
    <p>d.c // "c"</p>
    <p>d.b // "b"</p>
    <p>d.a // "a"</p>
    <p>上面代码返回一个新的对象d，代表了对象a和b被混入了对象c的操作。</p>
    <p>出于完整性的考虑，Object.getOwnPropertyDescriptors()进入标准以后，以后还会新增Reflect.getOwnPropertyDescriptors()方法。</p>
    <p>__proto__属性，Object.setPrototypeOf()，Object.getPrototypeOf()</p>
    <p>JavaScript 语言的对象继承是通过原型链实现的。ES6 提供了更多原型对象的操作方法。</p>
    <p>__proto__属性</p>
    <p>__proto__属性（前后各两个下划线），用来读取或设置当前对象的prototype对象。目前，所有浏览器（包括 IE11）都部署了这个属性。</p>
    <p>// es5 的写法</p>
    <p>const obj = {</p>
    <p>    method: function() { ... }</p>
    <p>};</p>
    <p>obj.__proto__ = someOtherObj;</p>
    <p>// es6 的写法</p>
    <p>var obj = Object.create(someOtherObj);</p>
    <p>obj.method = function() { ... };</p>
    <p>该属性没有写入 ES6 的正文，而是写入了附录，原因是__proto__前后的双下划线，说明它本质上是一个内部属性，而不是一个正式的对外的 API，只是由于浏览器广泛支持，才被加入了 ES6。标准明确规定，只有浏览器必须部署这个属性，其他运行环境不一定需要部署，而且新的代码最好认为这个属性是不存在的。因此，无论从语义的角度，还是从兼容性的角度，都不要使用这个属性，而是使用下面的Object.setPrototypeOf()（写操作）、Object.getPrototypeOf()（读操作）、Object.create()（生成操作）代替。</p>
    <p>实现上，__proto__调用的是Object.prototype.__proto__，具体实现如下。</p>
    <p>Object.defineProperty(Object.prototype, '__proto__', {</p>
    <p>    get() {</p>
    <p>        let _thisObj = Object(this);</p>
    <p>        return Object.getPrototypeOf(_thisObj);</p>
    <p>    },</p>
    <p>    set(proto) {</p>
    <p>        if (this === undefined || this === null) {</p>
    <p>            throw new TypeError();</p>
    <p>        }</p>
    <p>        if (!isObject(this)) {</p>
    <p>            return undefined;</p>
    <p>        }</p>
    <p>        if (!isObject(proto)) {</p>
    <p>            return undefined;</p>
    <p>        }</p>
    <p>        let status = Reflect.setPrototypeOf(this, proto);</p>
    <p>        if (!status) {</p>
    <p>            throw new TypeError();</p>
    <p>        }</p>
    <p>    },</p>
    <p>});</p>
    <p>function isObject(value) {</p>
    <p>    return Object(value) === value;</p>
    <p>}</p>
    <p>如果一个对象本身部署了__proto__属性，该属性的值就是对象的原型。</p>
    <p>Object.getPrototypeOf({ __proto__: null })</p>
    <p>// null</p>
    <p>Object.setPrototypeOf()</p>
    <p>Object.setPrototypeOf方法的作用与__proto__相同，用来设置一个对象的prototype对象，返回参数对象本身。它是 ES6 正式推荐的设置原型对象的方法。</p>
    <p>// 格式</p>
    <p>Object.setPrototypeOf(object, prototype)</p>
    <p>// 用法</p>
    <p>const o = Object.setPrototypeOf({}, null);</p>
    <p>该方法等同于下面的函数。</p>
    <p>function setPrototypeOf(obj, proto) {</p>
    <p>    obj.__proto__ = proto;</p>
    <p>    return obj;</p>
    <p>}</p>
    <p>下面是一个例子。</p>
    <p>let proto = {};</p>
    <p>let obj = { x: 10 };</p>
    <p>Object.setPrototypeOf(obj, proto);</p>
    <p>proto.y = 20;</p>
    <p>proto.z = 40;</p>
    <p>obj.x // 10</p>
    <p>obj.y // 20</p>
    <p>obj.z // 40</p>
    <p>上面代码将proto对象设为obj对象的原型，所以从obj对象可以读取proto对象的属性。</p>
    <p>如果第一个参数不是对象，会自动转为对象。但是由于返回的还是第一个参数，所以这个操作不会产生任何效果。</p>
    <p>Object.setPrototypeOf(1, {}) === 1 // true</p>
    <p>Object.setPrototypeOf('foo', {}) === 'foo' // true</p>
    <p>Object.setPrototypeOf(true, {}) === true // true</p>
    <p>由于undefined和null无法转为对象，所以如果第一个参数是undefined或null，就会报错。</p>
    <p>Object.setPrototypeOf(undefined, {})</p>
    <p>// TypeError: Object.setPrototypeOf called on null or undefined</p>
    <p>Object.setPrototypeOf(null, {})</p>
    <p>// TypeError: Object.setPrototypeOf called on null or undefined</p>
    <p>Object.getPrototypeOf()</p>
    <p>该方法与Object.setPrototypeOf方法配套，用于读取一个对象的原型对象。</p>
    <p>Object.getPrototypeOf(obj);</p>
    <p>下面是一个例子。</p>
    <p>function Rectangle() {</p>
    <p>    // ...</p>
    <p>}</p>
    <p>const rec = new Rectangle();</p>
    <p>Object.getPrototypeOf(rec) === Rectangle.prototype</p>
    <p>// true</p>
    <p>Object.setPrototypeOf(rec, Object.prototype);</p>
    <p>Object.getPrototypeOf(rec) === Rectangle.prototype</p>
    <p>// false</p>
    <p>如果参数不是对象，会被自动转为对象。</p>
    <p>// 等同于 Object.getPrototypeOf(Number(1))</p>
    <p>Object.getPrototypeOf(1)</p>
    <p>// Number {[[PrimitiveValue]]: 0}</p>
    <p>// 等同于 Object.getPrototypeOf(String('foo'))</p>
    <p>Object.getPrototypeOf('foo')</p>
    <p>// String {length: 0, [[PrimitiveValue]]: ""}</p>
    <p>// 等同于 Object.getPrototypeOf(Boolean(true))</p>
    <p>Object.getPrototypeOf(true)</p>
    <p>// Boolean {[[PrimitiveValue]]: false}</p>
    <p>Object.getPrototypeOf(1) === Number.prototype // true</p>
    <p>Object.getPrototypeOf('foo') === String.prototype // true</p>
    <p>Object.getPrototypeOf(true) === Boolean.prototype // true</p>
    <p>如果参数是undefined或null，它们无法转为对象，所以会报错。</p>
    <p>Object.getPrototypeOf(null)</p>
    <p>// TypeError: Cannot convert undefined or null to object</p>
    <p>Object.getPrototypeOf(undefined)</p>
    <p>// TypeError: Cannot convert undefined or null to object</p>
    <p>Object.keys()，Object.values()，Object.entries()</p>
    <p>Object.keys()</p>
    <p>ES5 引入了Object.keys方法，返回一个数组，成员是参数对象自身的（不含继承的）所有可遍历（enumerable）属性的键名。</p>
    <p>var obj = { foo: 'bar', baz: 42 };</p>
    <p>Object.keys(obj)</p>
    <p>// ["foo", "baz"]</p>
    <p>ES2017 引入了跟Object.keys配套的Object.values和Object.entries，作为遍历一个对象的补充手段，供for...of循环使用。</p>
    <p>let {keys, values, entries} = Object;</p>
    <p>let obj = { a: 1, b: 2, c: 3 };</p>
    <p>for (let key of keys(obj)) {</p>
    <p>    console.log(key); // 'a', 'b', 'c'</p>
    <p>}</p>
    <p>for (let value of values(obj)) {</p>
    <p>    console.log(value); // 1, 2, 3</p>
    <p>}</p>
    <p>for (let [key, value] of entries(obj)) {</p>
    <p>    console.log([key, value]); // ['a', 1], ['b', 2], ['c', 3]</p>
    <p>}</p>
    <p>Object.values()</p>
    <p>Object.values方法返回一个数组，成员是参数对象自身的（不含继承的）所有可遍历（enumerable）属性的键值。</p>
    <p>const obj = { foo: 'bar', baz: 42 };</p>
    <p>Object.values(obj)</p>
    <p>// ["bar", 42]</p>
    <p>返回数组的成员顺序，与本章的《属性的遍历》部分介绍的排列规则一致。</p>
    <p>const obj = { 100: 'a', 2: 'b', 7: 'c' };</p>
    <p>Object.values(obj)</p>
    <p>// ["b", "c", "a"]</p>
    <p>上面代码中，属性名为数值的属性，是按照数值大小，从小到大遍历的，因此返回的顺序是b、c、a。</p>
    <p>Object.values只返回对象自身的可遍历属性。</p>
    <p>const obj = Object.create({}, {p: {value: 42}});</p>
    <p>Object.values(obj) // []</p>
    <p>上面代码中，Object.create方法的第二个参数添加的对象属性（属性p），如果不显式声明，默认是不可遍历的，因为p的属性描述对象的enumerable默认是false，Object.values不会返回这个属性。只要把enumerable改成true，Object.values就会返回属性p的值。</p>
    <p>const obj = Object.create({}, {p:</p>
    <p>    {</p>
    <p>        value: 42,</p>
    <p>        enumerable: true</p>
    <p>    }</p>
    <p>});</p>
    <p>Object.values(obj) // [42]</p>
    <p>Object.values会过滤属性名为 Symbol 值的属性。</p>
    <p>Object.values({ [Symbol()]: 123, foo: 'abc' });</p>
    <p>// ['abc']</p>
    <p>如果Object.values方法的参数是一个字符串，会返回各个字符组成的一个数组。</p>
    <p>Object.values('foo')</p>
    <p>// ['f', 'o', 'o']</p>
    <p>上面代码中，字符串会先转成一个类似数组的对象。字符串的每个字符，就是该对象的一个属性。因此，Object.values返回每个属性的键值，就是各个字符组成的一个数组。</p>
    <p>如果参数不是对象，Object.values会先将其转为对象。由于数值和布尔值的包装对象，都不会为实例添加非继承的属性。所以，Object.values会返回空数组。</p>
    <p>Object.values(42) // []</p>
    <p>Object.values(true) // []</p>
    <p>Object.entries()</p>
    <p>Object.entries()方法返回一个数组，成员是参数对象自身的（不含继承的）所有可遍历（enumerable）属性的键值对数组。</p>
    <p>const obj = { foo: 'bar', baz: 42 };</p>
    <p>Object.entries(obj)</p>
    <p>// [ ["foo", "bar"], ["baz", 42] ]</p>
    <p>除了返回值不一样，该方法的行为与Object.values基本一致。</p>
    <p>如果原对象的属性名是一个 Symbol 值，该属性会被忽略。</p>
    <p>Object.entries({ [Symbol()]: 123, foo: 'abc' });</p>
    <p>// [ [ 'foo', 'abc' ] ]</p>
    <p>上面代码中，原对象有两个属性，Object.entries只输出属性名非 Symbol 值的属性。将来可能会有Reflect.ownEntries()方法，返回对象自身的所有属性。</p>
    <p>Object.entries的基本用途是遍历对象的属性。</p>
    <p>let obj = { one: 1, two: 2 };</p>
    <p>for (let [k, v] of Object.entries(obj)) {</p>
    <p>    console.log(</p>
    <p>        `${JSON.stringify(k)}: ${JSON.stringify(v)}`</p>
    <p>    );</p>
    <p>}</p>
    <p>// "one": 1</p>
    <p>// "two": 2</p>
    <p>Object.entries方法的另一个用处是，将对象转为真正的Map结构。</p>
    <p>const obj = { foo: 'bar', baz: 42 };</p>
    <p>const map = new Map(Object.entries(obj));</p>
    <p>map // Map { foo: "bar", baz: 42 }</p>
    <p>自己实现Object.entries方法，非常简单。</p>
    <p>// Generator函数的版本</p>
    <p>function* entries(obj) {</p>
    <p>    for (let key of Object.keys(obj)) {</p>
    <p>        yield [key, obj[key]];</p>
    <p>    }</p>
    <p>}</p>
    <p>// 非Generator函数的版本</p>
    <p>function entries(obj) {</p>
    <p>    let arr = [];</p>
    <p>    for (let key of Object.keys(obj)) {</p>
    <p>        arr.push([key, obj[key]]);</p>
    <p>    }</p>
    <p>    return arr;</p>
    <p>}</p>
    <p>Object.fromEntries()</p>
    <p>Object.fromEntries()方法是Object.entries()的逆操作，用于将一个键值对数组转为对象。</p>
    <p>Object.fromEntries([</p>
    <p>    ['foo', 'bar'],</p>
    <p>    ['baz', 42]</p>
    <p>])</p>
    <p>// { foo: "bar", baz: 42 }</p>
    <p>该方法的主要目的，是将键值对的数据结构还原为对象，因此特别适合将 Map 结构转为对象。</p>
    <p>// 例一</p>
    <p>const entries = new Map([</p>
    <p>    ['foo', 'bar'],</p>
    <p>    ['baz', 42]</p>
    <p>]);</p>
    <p>Object.fromEntries(entries)</p>
    <p>// { foo: "bar", baz: 42 }</p>
    <p>// 例二</p>
    <p>const map = new Map().set('foo', true).set('bar', false);</p>
    <p>Object.fromEntries(map)</p>
    <p>// { foo: true, bar: false }</p>
    <p>该方法的一个用处是配合URLSearchParams对象，将查询字符串转为对象。</p>
    <p>Object.fromEntries(new URLSearchParams('foo=bar&baz=qux'))</p>
    <p>// { foo: "bar", baz: "qux" }</p>
</div>
